package me.magicall.db.springjdbc;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import me.magicall.coll.ElementTransformer;
import me.magicall.coll.ElementTransformerUtil.SerializableElementTransformer;
import me.magicall.convenient.ResultView;
import me.magicall.db.dao.Dao;
import me.magicall.db.meta.DbColumn;
import me.magicall.db.meta.ForeignKey;
import me.magicall.db.meta.TableMeta;
import me.magicall.db.meta.TableMetaAccessor;
import me.magicall.db.model.BaseModel.MapModel;
import me.magicall.db.model.BaseModel.MapModel.LongIdMapModel;
import me.magicall.db.util.DbUtil;
import me.magicall.db.util.PageInfo;
import me.magicall.db.util.ReturnKeyAndAffect;
import me.magicall.util.ObjectUtil;
import me.magicall.util.kit.Kits;

public class MapModelAccessorImpl implements MapModelAccessor {

	private static final String ALL_FIELDS = "allFields";
	private static final char PROPERTY_SEPERATOR_CHAR = '.';
	private static final String PROPERTY_SEPERATOR = String.valueOf(PROPERTY_SEPERATOR_CHAR);

	private TableMetaAccessor tableMetaAccessor;
	private DaoFactory daoFactory;

	public MapModelAccessorImpl(final TableMetaAccessor tableMetaAccessor, final DaoFactory daoFactory) {
		super();
		this.tableMetaAccessor = tableMetaAccessor;
		this.daoFactory = daoFactory;
	}

	@Override
	public MapModel<?> getOne(final String modelName, final Map<String, ?> parameterMap) {
		final String tableName = DbUtil.javaNameToDbName(modelName);

		final Map<String, Object> finalParamMap = buildFinalParamMap(tableName, parameterMap);//
		if (finalParamMap == null) {
			return null;
		}

		final List<MapModel<?>> list = daoGetList(tableName, finalParamMap, PageInfo.FIRST_ONE);//
		return list.isEmpty() ? null : list.get(0);
	}

	@Override
	public List<MapModel<?>> list(final String modelName, final Map<String, ?> parameterMap, final PageInfo pageInfo) {
		final String tableName = DbUtil.javaNameToDbName(modelName);

		final Map<String, Object> finalParamMap = buildFinalParamMap(tableName, parameterMap);//
		if (finalParamMap == null) {
			return Kits.LIST.emptyValue();
		}

		final List<MapModel<?>> list = daoGetList(tableName, finalParamMap, pageInfo);//
		if (Kits.LIST.isEmpty(list)) {
			return Kits.LIST.emptyValue();
		}

		return list;
	}

	@Override
	public int count(final String modelName, final Map<String, ?> parameterMap) {
		final String tableName = DbUtil.javaNameToDbName(modelName);
		final Map<String, Object> finalParamMap = buildFinalParamMap(tableName, parameterMap);//
		if (finalParamMap == null) {
			return 0;
		}
		return getMapModelDao(tableName).count(finalParamMap);
	}

	@Override
	public ResultView<MapModel<?>> listWithPageInfo(final String modelName, final Map<String, ?> parameterMap, final PageInfo pageInfo) {
		final String tableName = DbUtil.javaNameToDbName(modelName);

		final Map<String, Object> finalParamMap = buildFinalParamMap(tableName, parameterMap);//
		if (finalParamMap == null) {
			return ResultView.emptyResultView();
		}

		final List<MapModel<?>> list = list(modelName, finalParamMap, pageInfo);
		if (Kits.COLL.isEmpty(list)) {
			return ResultView.emptyResultView();
		}

		final ResultView<MapModel<?>> rt = new ResultView<MapModel<?>>(list);
		final int count = getMapModelDao(tableName).count(finalParamMap);
		rt.setTheNumbers(count, pageInfo.getOffset(), pageInfo.getSize());
		return rt;
	}

	private Map<String, Object> buildFinalParamMap(final String tableName, final Map<String, ?> parameterMap) {
		final Map<String, Object> finalParamMap = new HashMap<String, Object>();
		final Map<String, Map<String, Object>> refingParams = new HashMap<String, Map<String, Object>>();

		for (final Entry<String, ?> e : parameterMap.entrySet()) {
			final String key = e.getKey();
			final Object value = e.getValue();

			final int dotIndex = key.indexOf(PROPERTY_SEPERATOR);
			if (dotIndex == 0) {
				throw new IllegalArgumentException("property must not start with '" + PROPERTY_SEPERATOR + "'");
			} else if (dotIndex < 0) {
				finalParamMap.put(key, value);
			} else {//关联其他表的情况
				final String refingModelName = key.substring(0, dotIndex);
				final String refingModelFieldName = key.substring(dotIndex + PROPERTY_SEPERATOR.length());

				//FIXME:多个refingModelName相同的情况:测试
				Map<String, Object> refingMap = refingParams.get(refingModelName);
				if (refingMap == null) {
					refingMap = new HashMap<String, Object>();//Collections.singletonMap(refingModelFieldName, value);
					refingParams.put(refingModelName, refingMap);
				}
				refingMap.put(refingModelFieldName, value);
//				if (!refingOtherTable(tableName, finalParamMap, DbUtil.javaNameToDbName(refingModelName),//
//						refingMap)) {
//					return null;
//				}
			}
		}

		for (final Entry<String, Map<String, Object>> e : refingParams.entrySet()) {
			final String refingModelName = e.getKey();
			final Map<String, Object> refingParam = e.getValue();
			try {
				if (!refingOtherTable(tableName, finalParamMap, DbUtil.javaNameToDbName(refingModelName), refingParam)) {
					return null;
				}
			} catch (final IllegalStateException e1) {
			}
		}
		return finalParamMap;
	}

	/**
	 * 如果从关联的表中没有查询到数据,返回false,方便直接跳出调用栈.
	 * 
	 * @param tableName
	 * @param finalParamMap
	 * @param refingTableName
	 * @param refingTableParamMap
	 * @return
	 */
	private boolean refingOtherTable(final String tableName, final Map<String, Object> finalParamMap,//
			final String refingTableName, final Map<String, ? extends Object> refingTableParamMap) {
		final Collection<MapModel<?>> refingModels = daoGetList(refingTableName, buildFinalParamMap(refingTableName, refingTableParamMap),
				PageInfo.ALL_PAGE_INFO);
		if (Kits.COLL.isEmpty(refingModels)) {
			return false;
		}
		final TableMeta myTableMeta = tableMetaAccessor.getTableMetaUsingTableName(tableName);//比如tag
		final TableMeta refingTableMeta = tableMetaAccessor.getTableMetaUsingTableName(refingTableName);//比如tag_ref

		final ForeignKey refedForeignKey = findForeignKey(myTableMeta, refingTableMeta);
		if (refedForeignKey != null) {//直接关联的表，myTableMeta被refingModels外键所指
			finalParamMap.put(DbUtil.dbNameToJavaName(refedForeignKey.getReferencedColumn().getName()),//
					Kits.COLL.transform(refingModels, new ElementTransformer<MapModel<?>, Object>() {
						@Override
						public Object transform(final int index, final MapModel<?> element) {
							return element.get(DbUtil.dbNameToJavaName(refedForeignKey.getReferencingColumn().getName()));
						}
					}));
			return true;
		}

		final ForeignKey refingForeignKey = findForeignKey(refingTableMeta, myTableMeta);
		if (refingForeignKey != null) {
			finalParamMap.put(DbUtil.dbNameToJavaName(refingForeignKey.getReferencingColumn().getName()),//
					Kits.COLL.transform(refingModels, new ElementTransformer<MapModel<?>, Object>() {
						@Override
						public Object transform(final int index, final MapModel<?> element) {
							return element.get(DbUtil.dbNameToJavaName(refingForeignKey.getReferencedColumn().getName()));
						}
					}));
			return true;
		}

		//间接关联的表，用于多对多
		TableMeta multiMultiTable = null;//多对多的那个表
		ForeignKey multiMultiForeignKey = null;
		final Collection<ForeignKey> myRefedForeignKeys = myTableMeta.getReferencedForeignKeys();
		final Collection<ForeignKey> otherRefedForeignKeys = refingTableMeta.getReferencedForeignKeys();
		for (final ForeignKey f : myRefedForeignKeys) {
			final TableMeta t = f.getReferencingTable();
			for (final ForeignKey otherF : otherRefedForeignKeys) {
				final TableMeta otherT = otherF.getReferencingTable();
				if (otherT.equals(t)) {
					if (multiMultiTable == null) {
						multiMultiTable = t;
						multiMultiForeignKey = otherF;
					} else {
						throw new IllegalStateException("table '" + myTableMeta.getName() + "' and '" + refingTableMeta.getName()
								+ "' have multi Descartes table");
					}
				}
			}
		}
		if (multiMultiTable == null) {
			throw new IllegalStateException("table '" + myTableMeta.getName() + "' and '" + refingTableMeta.getName() + "' have no Descartes table");
		}
		class TF implements SerializableElementTransformer<MapModel<?>, Object> {
			private static final long serialVersionUID = 591872738447442486L;

			private final ForeignKey f;

			private TF(final ForeignKey f) {
				super();
				this.f = f;
			}

			@Override
			public Object transform(final int index, final MapModel<?> element) {
				return element.get(DbUtil.dbNameToJavaName(f.getReferencedColumn().getName()));
			}
		}
		return refingOtherTable(tableName, finalParamMap, multiMultiTable.getName(),//
				Collections.singletonMap(DbUtil.dbNameToJavaName(multiMultiForeignKey.getReferencingColumn().getName()),//
						Kits.COLL.transform(refingModels, new TF(multiMultiForeignKey))));
	}

	private List<MapModel<?>> daoGetList(final String tableName, final Map<String, ? extends Object> paramMap, final PageInfo pageInfo) {
		if (paramMap == null) {
			return Kits.LIST.emptyValue();
		}
		final List<MapModel<?>> list = getMapModelDao(tableName).getList(paramMap, pageInfo);
		final TableMeta tableMeta = tableMetaAccessor.getTableMetaUsingTableName(tableName);
		for (final MapModel<?> m : list) {
			m.set(ALL_FIELDS, tableMeta.getColumns());
		}
		return list;
	}

	private List<MapModel<?>> daoGetAll(final String tableName, final Map<String, ? extends Object> paramMap) {
		return daoGetList(tableName, paramMap, PageInfo.ALL_PAGE_INFO);
	}

	@Override
	public Collection<MapModel<?>> list(final String modelName, final Collection<? extends Object> ids) {
		final Collection<MapModel<?>> collection = getMapModelDao(modelName).listUsingIds(ids);
		final TableMeta tableMeta = tableMetaAccessor.getTableMetaUsingModelName(modelName);
		for (final MapModel<?> m : collection) {
			m.set(ALL_FIELDS, tableMeta.getColumns());
		}
		return collection;
	}

	@Override
	public Map<Comparable<?>, MapModel<?>> map(final String modelName, final Collection<? extends Object> ids) {
		return collToMap(list(modelName, ids));
	}

	private Map<Comparable<?>, MapModel<?>> collToMap(final Collection<MapModel<?>> models) {
		final Map<Comparable<?>, MapModel<?>> map = new LinkedHashMap<Comparable<?>, MapModel<?>>(models.size()) {
			private static final long serialVersionUID = 8101157555628055703L;

			@Override
			public MapModel<?> get(final Object key) {
				return key instanceof Number ? super.get(((Number) key).longValue()) : super.get(key);
			}

			@Override
			public MapModel<?> put(final Comparable<?> key, final MapModel<?> value) {
				return key instanceof Number ? super.put(((Number) key).longValue(), value) : super.put(key, value);
			}
		};
		for (final MapModel<?> m : models) {
			map.put(m.getId(), m);
		}
		return map;
	}

	@Override
	public MapModel<?> getOne(final String modelName, final Object id, final boolean withReferenced, final boolean withReferencing) {
		final Dao<MapModel<?>> dao = getMapModelDao(modelName);
		final MapModel<?> bean = new LongIdMapModel();
		bean.set(DbUtil.COMMON_ID_FIELD_NAME, id);
		dao.fill(bean);
		bean.set(ALL_FIELDS, tableMetaAccessor.getTableMetaUsingModelName(modelName).getColumns());
		return getOne0(modelName, id, withReferenced, withReferencing, bean);
	}

	private MapModel<?> getOne0(final String modelName, final Object id, final boolean withReferenced, final boolean withReferencing, final MapModel<?> bean) {
		final TableMeta tableMeta = tableMetaAccessor.getTableMetaIgnoreCase(modelName);
		if (withReferenced) {
			final Collection<ForeignKey> referencedForeignKeys = tableMeta.getReferencedForeignKeys();
			if (!Kits.COLL.isEmpty(referencedForeignKeys)) {
				for (final ForeignKey foreignKey : referencedForeignKeys) {
					final TableMeta referencingTable = foreignKey.getReferencingTable();//多对多那张表
					final String referencingTableName = referencingTable.getName();

					final Collection<ForeignKey> referencingTableForeignKeys = referencingTable.getForeignKeys();

					final Collection<MapModel<?>> referencingModels = daoGetAll(referencingTableName,//
							Collections.singletonMap(DbUtil.dbNameToJavaName(foreignKey.getReferencingColumn().getName()), id));//
					for (final ForeignKey f : referencingTableForeignKeys) {
						if (f == foreignKey) {
							bean.set(DbUtil.dbNameToJavaName(referencingTableName) + "s", referencingModels);
						} else {
							final Collection<Long> referencingModelIds = new ArrayList<Long>(referencingModels.size());
							for (final MapModel<?> referencingModel : referencingModels) {
								final DbColumn referencingColumn = f.getReferencingColumn();
								final Number number = (Number) referencingModel.get(DbUtil.dbNameToJavaName(referencingColumn.getName()));
								if (number != null) {
									referencingModelIds.add(number.longValue());
								}
							}
							final String name = DbUtil.dbNameToJavaName(f.getReferencedTable().getName());
							final Collection<MapModel<?>> coll = list(name, referencingModelIds);
							bean.set(name + "s", coll);
						}
					}
				}
			}
		}
		if (withReferencing) {
			final Collection<ForeignKey> foreignKeys = tableMeta.getForeignKeys();
			if (!Kits.COLL.isEmpty(foreignKeys)) {
				for (final ForeignKey foreignKey : foreignKeys) {
					final TableMeta referencedTable = foreignKey.getReferencedTable();
					final String referencedTableName = referencedTable.getName();
					final Collection<MapModel<?>> referencedModels = daoGetAll(referencedTableName,//
							Collections.singletonMap(DbUtil.dbNameToJavaName(foreignKey.getReferencedColumn().getName()),//
									bean.get(DbUtil.dbNameToJavaName(foreignKey.getReferencingColumn().getName()))));
					bean.set(DbUtil.dbNameToJavaName(referencedTableName) + "s", referencedModels);
				}
			}
		}
		return bean;
	}

	@Override
	public MapModel<?> getOne(final String modelName, final Object id) {
		return getOne(modelName, id, false, false);
	}

	@Override
	public ReturnKeyAndAffect addReturnKey(final String modelName, final Map<String, Object> parameterMap) {
		final MapModel<?> nbBean = new LongIdMapModel();

		final TableMeta tableMeta = tableMetaAccessor.getTableMetaUsingModelName(modelName);
		final List<String> fieldNames = DbUtil.dbNameToJavaName(tableMeta.getColumnNames());

		String multiFieldName = null;
		for (final String fieldName : fieldNames) {
			final Object value = parameterMap.get(fieldName);
			if (value != null) {
				if (ObjectUtil.isMultiValue(value)) {
					if (multiFieldName == null) {
						multiFieldName = fieldName;
					} else {
						throw new IllegalArgumentException("only one multi field value.now there is one:'" + multiFieldName + "' ,so '" + fieldName
								+ "' cannot be multi");
					}
				} else {
					nbBean.set(fieldName, value);
				}
			}
		}

		if (multiFieldName != null) {
			if (nbBean.getId() != null) {
				throw new IllegalArgumentException("cannot multi with id field.(id=" + nbBean.getId() + ")");
			}
			final List<Object> values = ObjectUtil.castToList(parameterMap.get(multiFieldName));
			final List<MapModel<?>> list = new ArrayList<MapModel<?>>(values.size());
			for (final Object v : values) {
				final MapModel<?> b = new LongIdMapModel();
				b.putAll(nbBean);
				b.set(multiFieldName, v);
				list.add(b);
			}
			final int[] is = getMapModelDao(modelName).add(list);
			int rt = 0;
			for (final int i : is) {
				rt += i;
			}
			return new ReturnKeyAndAffect(null, rt);
		} else {
			final ReturnKeyAndAffect affect = getMapModelDao(modelName).addReturnKey(nbBean);
			parameterMap.put(DbUtil.COMMON_ID_FIELD_NAME, affect.getReturnKey());
			return affect;
		}
	}

	@Override
	public boolean addOrUpdate(final String modelName, final Map<String, Object> parameterMap) {
		final MapModel<?> nbBean = new LongIdMapModel();

		final TableMeta tableMeta = tableMetaAccessor.getTableMetaUsingModelName(modelName);
		final List<String> fieldNames = DbUtil.dbNameToJavaName(tableMeta.getColumnNames());

		String multiFieldName = null;
		for (final String fieldName : fieldNames) {
			final Object value = parameterMap.get(fieldName);
			if (value != null) {
				if (ObjectUtil.isMultiValue(value)) {
					if (multiFieldName == null) {
						multiFieldName = fieldName;
					} else {
						throw new IllegalArgumentException("only one multi field value.now there is one:'" + multiFieldName + "' ,so '" + fieldName
								+ "' cannot be multi");
					}
				} else {
					nbBean.set(fieldName, value);
				}
			}
		}

		if (multiFieldName != null) {
			if (nbBean.getId() != null) {
				throw new IllegalArgumentException("cannot multi with id field.(id=" + nbBean.getId() + ")");
			}
			final List<Object> values = ObjectUtil.castToList(parameterMap.get(multiFieldName));
			final List<MapModel<?>> list = new ArrayList<MapModel<?>>(values.size());
			for (final Object v : values) {
				final MapModel<?> b = new LongIdMapModel();
				b.putAll(nbBean);
				b.set(multiFieldName, v);
				list.add(b);
			}
			final int[] is = getMapModelDao(modelName).add(list);
			for (final int i : is) {
				if (i > 0) {
					return true;
				}
			}
			return false;
		} else {
			return getMapModelDao(modelName).addOrUpdate(nbBean);
		}
	}

	private ForeignKey findForeignKey(final TableMeta refedTableMeta, final TableMeta refingTableMeta) {
		final Collection<ForeignKey> foreignKeys = refingTableMeta.getForeignKeys();
		for (final ForeignKey f : foreignKeys) {
			if (f.getReferencedTable().equals(refedTableMeta)) {
				return f;
			}
		}
		return null;
	}

	private Dao<MapModel<?>> getMapModelDao(final String modelName) {
		return daoFactory.getMapModelDao(modelName);
	}

	public TableMetaAccessor getTableMetaAccessor() {
		return tableMetaAccessor;
	}

	public void setTableMetaAccessor(final TableMetaAccessor tableMetaAccessor) {
		this.tableMetaAccessor = tableMetaAccessor;
	}

	public DaoFactory getDaoFactory() {
		return daoFactory;
	}

	public void setDaoFactory(final DaoFactory daoFactory) {
		this.daoFactory = daoFactory;
	}

	@Override
	public List<MapModel<?>> all(final String modelName, final Map<String, ?> parameterMap) {
		return list(modelName, parameterMap, Integer.MAX_VALUE);
	}

	@Override
	public Map<Comparable<?>, MapModel<?>> allMap(final String modelName, final Map<String, ?> parameterMap) {
		return collToMap(all(modelName, parameterMap));
	}

	@Override
	public Collection<MapModel<?>> all(final String modelName, final Collection<? extends Object> ids) {
		return getMapModelDao(modelName).listUsingIds(ids);
	}

	@Override
	public Map<Comparable<?>, MapModel<?>> allMap(final String modelName, final Collection<? extends Object> ids) {
		return collToMap(all(modelName, ids));
	}

	@Override
	public Collection<MapModel<?>> all(final String modelName, final Object... ids) {
		return getMapModelDao(modelName).listUsingIds(ids);
	}

	@Override
	public Map<Comparable<?>, MapModel<?>> map(final String modelName, final Map<String, ?> parameterMap, final PageInfo pageInfo) {
		return collToMap(list(modelName, parameterMap, pageInfo));
	}

	@Override
	public ResultView<MapModel<?>> listWithPageInfo(final String modelName, final Map<String, ?> parameterMap, final int size) {
		return listWithPageInfo(modelName, parameterMap, PageInfo.offsetSize(0, size));
	}

	@Override
	public List<MapModel<?>> list(final String modelName, final Map<String, ?> parameterMap, final int size) {
		return list(modelName, parameterMap, PageInfo.offsetSize(0, size));
	}

	public Map<Comparable<?>, MapModel<?>> map(final String modelName, final Map<String, ?> parameterMap, final int size) {
		return collToMap(list(modelName, parameterMap, size));
	}

	@Override
	public Collection<MapModel<?>> all(final String modelName) {
		return getMapModelDao(modelName).all();
	}

	@Override
	public Map<Comparable<?>, MapModel<?>> allMap(final String modelName) {
		return collToMap(all(modelName));
	}

	@Override
	public boolean addOrUpdate(final String modelName, final Collection<MapModel<?>> models) {
		final int[] rt = getMapModelDao(modelName).addOrUpdate(models);

		for (final int i : rt) {
			if (i != 0) {
				return true;
			}
		}
		return false;
	}

	@Override
	public boolean containsModelName(final String modelName) {
		return daoFactory.containsModelName(modelName);
	}

	@Override
	public boolean del(final String modelName, final Comparable<?> id) {
		return getMapModelDao(modelName).del(id) > 0;
	}
}
